/*
 * (c) Copyright 2014 Hewlett-Packard Development Company, L.P. All rights reserved.
 */
package com.hp.snap.gr.db.voltdb;

import com.hp.snap.gr.exception.DatabaseException;
import com.hp.snap.gr.exception.RESTfulException;
import com.hp.snap.gr.utils.RESTfulUtils;
import com.hp.snap.gr.utils.Utils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * The Structure of the VoltDB JSON Response:
 * <p/>
 * <pre>
 * {   appstatus          (integer, boolean)
 *     appstatusstring    (string)
 *     exception          (integer)
 *     results            (array)
 *       [                (object, VoltTable)
 *         { data         (array)
 *             [          (any type)
 *             ]
 *           schema      (array)
 *             [ name    (string)
 *               type    (integer, enumerated)
 *             ]
 *           status      (integer, boolean)
 *         }
 *       ]
 *     status            (integer)
 *     statusstring      (string)
 * }
 * </pre>
 * <p/>
 * The key components of the JSON response are the following:
 * <p/>
 * <b>appstatus</b> Indicates the success or failure of the stored procedure. If appstatus is false, appstatusstring
 * contains the text of the status message.
 * <p/>
 * <b>results</b> An array of objects representing the data returned by the stored procedure. This is an array of
 * VoltTable objects. If the stored procedure does not return a value (i.e. is void or null), then results will be null.
 * <p/>
 * <b>data</b> Within each VoltTable object, data is the array of values.
 * <p/>
 * <b>schema</b> Within each VoltTable, object schema is an array of objects with two elements: the name of the field
 * and the datatype of that field (encoded as an enumerated integer value).
 * <p/>
 * <b>status</b> Indicates the success or failure of the VoltDB server in its attempt to execute the stored procedure.
 * The difference between appstatus and status is that if the server cannot execute the stored procedure, the status is
 * returned in status, whereas if the stored procedure can be invoked, but a failure occurs within the stored procedure
 * itself (such as a SQL constraint violation), the status is returned in appstatus.
 *
 * @author Yang, Lin
 * @since 3.2
 */
public class JSONHttpVoltDBClient {
    private VoltDBClientConfig config;

    private static final Logger logger = LoggerFactory.getLogger(JSONHttpVoltDBClient.class);

    public JSONHttpVoltDBClient(VoltDBClientConfig config) {
        this.config = config;
    }

    public JSONObject invokeStoredProcedure(String procedure, List<Object> params, boolean isThrowable) throws DatabaseException {

        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        nvps.add(new BasicNameValuePair("Procedure", procedure));

        if (CollectionUtils.isNotEmpty(params)) {
            /*
             * JSON encoded parameters which are an array (even if there is only one element to that array) and
             * therefore must be enclosed in square brackets.
             */
            nvps.add(new BasicNameValuePair("Parameters", encodeParameters(params)));
        }
        if (StringUtils.isNotBlank(config.getUser())) {
            nvps.add(new BasicNameValuePair("User", config.getUser()));
        }
        if (StringUtils.isNotBlank(config.getPassword())) {
            nvps.add(new BasicNameValuePair("Password", config.getPassword()));
        }
        if (StringUtils.isNotBlank(config.getHashedpassword())) {
            nvps.add(new BasicNameValuePair("Hashedpassword", config.getHashedpassword()));
        }
        if (null != config.getAdmin()) {
            nvps.add(new BasicNameValuePair("admin", config.getAdmin().toString()));
        }
        if (StringUtils.isNotBlank(config.getJsonp())) {
            nvps.add(new BasicNameValuePair("jsonp", config.getJsonp()));
        }

        JSONObject jsonResponse = null;
        try {
            HttpPost httpPost = new HttpPost(config.getUrl());
            httpPost.setEntity(new UrlEncodedFormEntity(nvps));

            HttpClient httpClient = RESTfulUtils.getHttpClient();
            HttpResponse httpResponse = httpClient.execute(httpPost);
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            String response = EntityUtils.toString(httpResponse.getEntity());

            if (statusCode >= 400) {
                throw new RESTfulException(statusCode, response);
            }

            jsonResponse = new JSONObject(response);
        } catch (RESTfulException e) {
            if (!isThrowable) {
                logger.warn("Response from server is not correct when calling stored procedure!");
                if (logger.isDebugEnabled()) {
                    logger.debug(Utils.stackTraceToString(e));
                }
            } else {
                throw new DatabaseException("Call VoltDB stored procedure failed!", e);
            }

        } catch (JSONException e) {
            if (!isThrowable) {
                logger.warn("Analyzing JSON message failure when calling stored procedure!");
                if (logger.isDebugEnabled()) {
                    logger.debug(Utils.stackTraceToString(e));
                }
            } else {
                throw new DatabaseException("Call VoltDB stored procedure failed!", e);
            }
        } catch (Exception e) {
            if (!isThrowable) {
                logger.warn("Unknow exception happened when calling stored procedure!");
                if (logger.isDebugEnabled()) {
                    logger.debug(Utils.stackTraceToString(e));
                }
            } else {
                throw new DatabaseException("Call VoltDB stored procedure failed!", e);
            }
        }

        return jsonResponse;
    }

    private String encodeParameters(List<Object> params) {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int ii = 0, nn = params.size(); ii < nn; ii++) {
            Object obj = params.get(ii);
            if (obj instanceof String) {
                sb.append(JSONObject.quote((String) obj));
            } else {
                sb.append(obj);
            }
            if (ii + 1 != nn) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    public VoltDBClientConfig getConfig() {
        return config;
    }

}
